/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2022-2022. All rights reserved.
 */

import * as FS from 'fs';
import * as PATH from 'path';

import { FileType } from './file-type.js';

function safeReadDirSync(path: string) {
  let dirData: string[] = [];
  try {
    dirData = FS.readdirSync(path);
  } catch (ex: any) {
    if (ex.code === 'EACCES' || ex.code === 'EPERM') {
      // User does not have permissions, ignore directory
    } else {
      throw ex;
    }
  }
  return dirData;
}

function normalizePath(path: string) {
  return path.replace(/\\/g, '/');
}

/**
 * File Visit options
 */
export interface FileOptions {
  maxDepth?: number;
  normalizePath?: boolean;
  filter?: RegExp; // name筛选通用正则
  extensions?: RegExp;
  exclude?: RegExp;
  followSymlinks?: boolean;
}

export class FileInfo {
  name: string;
  path: string;
  type: FileType;
  size: number;
  lastModifiedTime: number;
  isSymbolicLink = false;

  constructor(name: string, path: string, type: FileType = FileType.UNKNOWN, size = 0, lastModifiedTime = 0, isSymbolicLink = false) {
    this.name = name;
    this.path = path;
    this.type = type;
    this.size = size;
    this.lastModifiedTime = lastModifiedTime;
    this.isSymbolicLink = isSymbolicLink;
  }

  toString() {
    return `{name:${this.name},path:${this.path},type:${this.type},
        size:${this.size},lastModifiedTime:${this.lastModifiedTime},isSymbolicLink:${this.isSymbolicLink}`;
  }
}

export interface FileVisitor {
  preVisitDirectory(fileInfo: FileInfo): void;

  visitFile(fileInfo: FileInfo): void;

  visitFileFailed(path: string, error: any): void;

  postVisitDirectory(fileInfo: FileInfo): void;
}

function walkFile(path: string, item: FileInfo, fileVisitor: FileVisitor, options: FileOptions = {}) {
  const ext = PATH.extname(path).toLowerCase();

  // Skip if it does not match the extension regex
  if (options.extensions && !options.extensions.test(ext)) {
    return;
  }

  fileVisitor.visitFile(item);
}

function walkDirectory(path: string, currentDepth: number, item: FileInfo, fileVisitor: FileVisitor, options: FileOptions = {}, symlinks: any[]) {
  const dirData = safeReadDirSync(path);

  if (options.maxDepth === undefined || options.maxDepth > currentDepth) {
    fileVisitor.preVisitDirectory(item);
    for (const subDir of dirData) {
      walkFileRecursive(PATH.join(path, subDir), currentDepth + 1, fileVisitor, options, symlinks);
    }
    fileVisitor.postVisitDirectory(item);
  }
}

function walkFileRecursive(path: string, currentDepth = 0, fileVisitor: FileVisitor, options: FileOptions = {}, symlinks: any[]) {
  path = options.normalizePath ? normalizePath(path) : path;
  let stats: FS.Stats;
  let lstat: FS.Stats;

  try {
    stats = FS.statSync(path);
    lstat = FS.lstatSync(path);
  } catch (e) {
    fileVisitor.visitFileFailed(path, e);
    return;
  }

  // Skip if it matches the exclude regex
  if (options.exclude && options.exclude.test(path)) {
    return;
  }

  // name filter regexp
  if (options.filter && !options.filter.test(path)) {
    return;
  }

  const fileType = stats.isFile() ? FileType.FILE : FileType.DIRECTORY;
  const item = new FileInfo(PATH.basename(path), path, fileType, stats.size, stats.mtime.getTime(), lstat.isSymbolicLink());

  if (lstat.isSymbolicLink()) {
    // Skip if symbolic links should not be followed
    if (options.followSymlinks === false) {
      return;
    }

    // Skip if a cyclic symbolic link has been found
    if (symlinks.find((ino) => ino === lstat.ino)) {
      return;
    } else {
      symlinks.push(lstat.ino);
    }
  }

  if (stats.isFile()) {
    walkFile(path, item, fileVisitor, options);
  } else if (stats.isDirectory()) {
    walkDirectory(path, currentDepth, item, fileVisitor, options, symlinks);
  }
}

/**
 * Walk file tree in depth first recursive way
 *
 * @param path file or directory path
 * @param fileVisitor file visitor (depth first)
 * @param options file options
 */
export function walkFileTree(path: string, fileVisitor: FileVisitor, options: FileOptions = {}) {
  const symlinks: any[] = [];
  walkFileRecursive(path, 0, fileVisitor, options, symlinks);
}
